package de.tum.wi.som.simulation;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;

import umontreal.iro.lecuyer.probdist.BetaDist;
import umontreal.iro.lecuyer.probdist.Distribution;
import umontreal.iro.lecuyer.probdist.ExponentialDist;
import umontreal.iro.lecuyer.probdist.UniformDist;
import umontreal.iro.lecuyer.randvar.BetaGen;
import umontreal.iro.lecuyer.randvar.ExponentialGen;
import umontreal.iro.lecuyer.randvar.RandomVariateGen;
import umontreal.iro.lecuyer.randvar.UniformGen;
import umontreal.iro.lecuyer.rng.LFSR113;
import umontreal.iro.lecuyer.rng.RandomStream;
import au.com.bytecode.opencsv.CSVWriter;
import de.tum.wi.som.MainConstants;
import de.tum.wi.som.Node;
import de.tum.wi.som.Project;
import de.tum.wi.som.prio.PriorityRule;
import de.tum.wi.som.util.RCPFileUtil;

public class MonteCarlo {

	/**
	 * Runs the MonteCarlo Simulations. A normalized average project duration is
	 * calculated for each project using different priority rules. Activity
	 * based and Resource Based Policies are applied.
	 * 
	 * The output consists of 2 CSV files for Activity Based and Resource Based
	 * Policies. Projects are the columns, priority rules are the rows.
	 * 
	 * @throws IOException
	 */
	public void run() throws IOException {

		Date simulationStart = new Date();

		// List<PriorityRule> prioRules = new ArrayList<PriorityRule>();
		// prioRules.add(new MostTotalSuccessors());
		List<PriorityRule> prioRules = getPriorityRules();

		List<Project> projects = RCPFileUtil
				.loadAllFiles(MainConstants.J120_PROJECT_DIRECTORY);
		generateDeterministicActivityDurations(projects);

		double avgProjectDurationActivityBased;
		double avgProjectDurationResourceBased;
		double activityMakespanVariance;
		double resourceMakespanVariance;
		double avgResourceStrength;
		double avgResourceFactor;
		double avgNetworkComplexity;

		Date date = new Date();
		SimpleDateFormat formattedDate = new SimpleDateFormat(
				"yy-MM-dd-HH-mm-ss_");

		CSVWriter csvWriter;
		csvWriter = new CSVWriter(new FileWriter(
				MainConstants.RESULTS_FILE_PATH + formattedDate.format(date)
						+ MainConstants.RESULTS_FILE_NAME));

		// String[] containing all results for activity based policies/ resource
		// based policies
		String[] header = { "Priority Rule", "Project", "Policy",
				"Average normalized makespan", "Variance", "Resource Strength",
				"Resource Factor", "Network Complexity" };
		csvWriter.writeNext(header);
		csvWriter.flush();
		int prios = 0;
		for (PriorityRule prioRule : prioRules) {
			System.out.println("Starting priority rule " + prioRule.toString());
			int remaining = prioRules.size() - prios;
			System.out.println("Remaining priority rules: " + remaining
					+ ". Time to finish ca. " + (remaining * 49) / 60 + " h, "
					+ (remaining * 95) % 60 + " min.");

			int projectNumber = 0;

			for (Project project : projects) {
				avgProjectDurationActivityBased = 0;
				avgProjectDurationResourceBased = 0;
				activityMakespanVariance = 0;
				resourceMakespanVariance = 0;
				List<Double> resourceMakespans = new ArrayList<Double>();
				List<Double> activityMakespans = new ArrayList<Double>();
				avgResourceStrength = 0;
				avgResourceFactor = 0;
				avgNetworkComplexity = 0;
				projectNumber++;
				System.out.print("    Starting Project " + projectNumber
						+ "...");
				Date start = new Date();

				project.calculateAnchor();
				List<Project> stochasticProjects = getProjectsWithStochasticActivityDurations(project);

				for (Project stochasticProject : stochasticProjects) {

					if (avgResourceStrength == 0) {
						avgResourceStrength = Math.round(stochasticProject
								.calcRS() * 100.0) / 100.0;
					} else {
						avgResourceStrength = Math
								.round((avgResourceStrength + stochasticProject
										.calcRS()) / 2 * 100.0) / 100.0;
					}

					if (avgResourceFactor == 0) {
						avgResourceFactor = Math.round(stochasticProject
								.calcRF() * 100.0) / 100.0;
					} else {
						avgResourceFactor = Math
								.round((avgResourceFactor + stochasticProject
										.calcRF()) / 2 * 100.0) / 100.0;
					}

					if (avgNetworkComplexity == 0) {
						avgNetworkComplexity = Math.round(stochasticProject
								.calcC() * 100.0) / 100.0;
					} else {
						avgNetworkComplexity = Math
								.round((avgNetworkComplexity + stochasticProject
										.calcC()) / 2 * 100.0) / 100.0;
					}

					for (int i = 0; i < MainConstants.NUMBER_OF_SCENARIOS; i++) {

						Project projectCopy = stochasticProject
								.getCopyOfProject();

						prioRule.prioritize(projectCopy);

						Project projectCopy2 = projectCopy.getCopyOfProject();

						// run activity based policy
						avgProjectDurationActivityBased = runActivityBasedPolicy(
								projectCopy, avgProjectDurationActivityBased,
								activityMakespans);

						// run resource based policy
						avgProjectDurationResourceBased = runResourceBasedPolicy(
								projectCopy2, avgProjectDurationResourceBased,
								resourceMakespans);

					}
				}

				// round to two values and add it to the results of the priority
				// rule
				avgProjectDurationActivityBased = Math
						.round(avgProjectDurationActivityBased * 100.0) / 100.0;

				activityMakespanVariance = Math
						.round(computeVariance(activityMakespans,
								avgProjectDurationActivityBased) * 100.0) / 100.0;
				;
				String[] activityBasedResults = { prioRule.toString(),
						project.getName(), "activity-based",
						String.valueOf(avgProjectDurationActivityBased),
						String.valueOf(activityMakespanVariance),
						String.valueOf(avgResourceStrength),
						String.valueOf(avgResourceFactor),
						String.valueOf(avgNetworkComplexity) };
				csvWriter.writeNext(activityBasedResults);

				avgProjectDurationResourceBased = Math
						.round(avgProjectDurationResourceBased * 100.0) / 100.0;

				resourceMakespanVariance = Math
						.round(computeVariance(resourceMakespans,
								avgProjectDurationResourceBased) * 100.0) / 100.0;
				String[] resourceBasedResults = { prioRule.toString(),
						project.getName(), "resource-based",
						String.valueOf(avgProjectDurationResourceBased),
						String.valueOf(resourceMakespanVariance),
						String.valueOf(avgResourceStrength),
						String.valueOf(avgResourceFactor),
						String.valueOf(avgNetworkComplexity) };
				csvWriter.writeNext(resourceBasedResults);
				csvWriter.flush();
				Date finish = new Date();
				long seconds = (finish.getTime() - start.getTime()) / (1000);
				System.out.print("took " + seconds + " seconds time.\n");
			}
			prios++;
		}
		csvWriter.close();
		Date simulationEnd = new Date();
		long minutes = (simulationEnd.getTime() - simulationStart.getTime())
				/ (1000 * 60);
		System.out.println("-----------------------------------------");
		System.out.println("Simulation took " + minutes + " minutes in total.");

	}

	private void generateDeterministicActivityDurations(List<Project> projects) {
		for (Project project : projects) {
			Random random = new Random();
			for (Node node : project.getNodes()) {
				double d = random.nextInt(10) + 1;
				node.setD(d);
			}
		}

	}

	protected double computeVariance(List<Double> values, double average) {
		double variance = 0;
		for (Double value : values) {
			variance += Math.pow(value - average, 2);
		}
		return variance / values.size();
	}

	private List<Project> getProjectsWithStochasticActivityDurations(
			Project project) {
		List<Project> projects = new ArrayList<Project>();
		projects.add(getProjectWithStochasticActivityDurations(project,
				ProbabilityDistribution.UNIFORM_1));
		projects.add(getProjectWithStochasticActivityDurations(project,
				ProbabilityDistribution.UNIFORM_2));
		projects.add(getProjectWithStochasticActivityDurations(project,
				ProbabilityDistribution.EXPONENTIAL));
		projects.add(getProjectWithStochasticActivityDurations(project,
				ProbabilityDistribution.BETA_1));
		projects.add(getProjectWithStochasticActivityDurations(project,
				ProbabilityDistribution.BETA_2));
		return projects;
	}

	private Project getProjectWithStochasticActivityDurations(Project project,
			ProbabilityDistribution selectedDistribution) {
		Project stochasticProject = project.getCopyOfProject();
		for (Node n : stochasticProject.getNodes()) {
			double stochasticDuration = getStochasticProjectDuration(n.getD(),
					selectedDistribution);
			n.setD(stochasticDuration);
		}
		return stochasticProject;
	}

	private double getStochasticProjectDuration(double d,
			ProbabilityDistribution selectedDistribution) {
		Distribution distribution;
		RandomStream stream = new LFSR113();
		RandomVariateGen generator;
		switch (selectedDistribution) {
		case UNIFORM_1:
			distribution = new UniformDist(d - Math.sqrt(d), d + Math.sqrt(d));
			generator = new UniformGen(stream, (UniformDist) distribution);
			break;
		case UNIFORM_2:
			distribution = new UniformDist(0, 2 * d);
			generator = new UniformGen(stream, (UniformDist) distribution);
			break;
		case EXPONENTIAL:
			distribution = new ExponentialDist(d);
			generator = new ExponentialGen(stream,
					(ExponentialDist) distribution);
			break;
		case BETA_1:
			double alpha = d / 2.0 - 1.0 / 3.0;
			double beta = 2.0 * alpha;
			distribution = new BetaDist(alpha, beta, d / 2.0, 2.0 * d);
			generator = new BetaGen(stream, (BetaDist) distribution);
			break;
		case BETA_2:
			double alpha2 = 1.0 / 6.0;
			double beta2 = 2.0 * alpha2;
			distribution = new BetaDist(alpha2, beta2, d / 2.0, 2.0 * d);
			generator = new BetaGen(stream, (BetaDist) distribution);
			break;
		default:
			throw new IllegalArgumentException();
		}
		return generator.nextDouble();
	}

	private double runResourceBasedPolicy(Project project,
			double avgProjectDurationResourceBased,
			List<Double> resourceMakspans) {
		project.parallelSGS(project.getNodes());

		// normalize result
		double normalizedDurationResourceBased = project.getMakespan()
				/ project.getAnchor();
		resourceMakspans.add(normalizedDurationResourceBased);

		if (avgProjectDurationResourceBased == 0) {
			avgProjectDurationResourceBased = normalizedDurationResourceBased;
		} else {
			avgProjectDurationResourceBased = (avgProjectDurationResourceBased + normalizedDurationResourceBased) / 2;
		}

		return avgProjectDurationResourceBased;

	}

	private List<PriorityRule> getPriorityRules() {
		List<PriorityRule> prioRules = new ArrayList<PriorityRule>();

		// fetch pr implementations
		Path p = Paths.get(System.getProperty("user.dir"), "src", "main", "de",
				"tum", "wi", "som", "prio", "impl");
		File dir = new File(p.toUri());
		String[] l = dir.list();

		for (String pr : l) {
			// skip svn directory
			if (pr.contains(".svn"))
				continue;

			// remove file prefix
			pr = pr.replace(".java", "");
			// call pr
			Class<?> c = null;
			PriorityRule prioRule = null;
			try {
				c = Class.forName(MainConstants.PQ_PATH + "." + pr);
				prioRule = (PriorityRule) c.newInstance();
				prioRules.add(prioRule);
			} catch (ClassNotFoundException e) {
				System.err.println("Class could not be found");
			} catch (InstantiationException | IllegalAccessException e2) {
				System.err.println("Class could not be instanciated");
			}
		}

		return prioRules;

	}

	private double runActivityBasedPolicy(Project project,
			double avgProjectDurationActivityBased,
			List<Double> activityMakespans) {
		project.stochasticSerialSGS(project.getNodes());

		// normalize result
		double normalizedDurationActivityBased = project.getMakespan()
				/ project.getAnchor();
		activityMakespans.add(normalizedDurationActivityBased);

		if (avgProjectDurationActivityBased == 0) {
			avgProjectDurationActivityBased = normalizedDurationActivityBased;
		} else {
			avgProjectDurationActivityBased = (avgProjectDurationActivityBased + normalizedDurationActivityBased) / 2;
		}

		return avgProjectDurationActivityBased;
	}
}
